/*******************************************************************************
 * Copyright (c) 2011 MadRobot.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser Public License v2.1
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * 
 * Contributors:
 *  Elton Kent - initial API and implementation
 ******************************************************************************/
package com.madrobot.reflect;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;


/**
 * <p>
 * Assists in implementing {@link Object#toString()} methods using reflection.
 * </p>
 * 
 * <p>
 * This class uses reflection to determine the fields to append. Because these
 * fields are usually private, the class uses
 * {@link java.lang.reflect.AccessibleObject#setAccessible(java.lang.reflect.AccessibleObject[], boolean)}
 * to change the visibility of the fields. This will fail under a security
 * manager, unless the appropriate permissions are set up correctly.
 * </p>
 * 
 * <p>
 * A typical invocation for this method would look like:
 * </p>
 * 
 * <pre>
 * public String toString() {
 * 	return ReflectionToStringBuilder.toString(this);
 * }
 * </pre>
 * 
 * 
 * 
 * <p>
 * You can also use the builder to debug 3rd party objects:
 * </p>
 * 
 * <pre>
 * System.out.println(&quot;An object: &quot; + ReflectionToStringBuilder.toString(anObject));
 * </pre>
 * 
 * 
 * 
 * <p>
 * A subclass can control field output by overriding the methods:
 * <ul>
 * <li>{@link #accept(java.lang.reflect.Field)}</li>
 * <li>{@link #getValue(java.lang.reflect.Field)}</li>
 * </ul>
 * </p>
 * <p>
 * For example, this method does <i>not</i> include the <code>password</code>
 * field in the returned <code>String</code>:
 * </p>
 * 
 * <pre>
 * public String toString() {
 * 	return (new ReflectionToStringBuilder(this) {
 * 		protected boolean accept(Field f) {
 * 			return super.accept(f) &amp;&amp; !f.getName().equals(&quot;password&quot;);
 * 		}
 * 	}).toString();
 * }
 * </pre>
 * 
 * 
 * 
 * <p>
 * The exact format of the <code>toString</code> is determined by the
 * {@link ToStringStyle} passed into the constructor.
 * </p>
 */
class ReflectionToStringBuilder extends ToStringBuilder {

	/**
	 * <p>
	 * Builds a <code>toString</code> value using the default
	 * <code>ToStringStyle</code> through reflection.
	 * </p>
	 * 
	 * <p>
	 * It uses <code>AccessibleObject.setAccessible</code> to gain access to
	 * private fields. This means that it will throw a security exception if run
	 * under a security manager, if the permissions are not set up correctly. It
	 * is also not as efficient as testing explicitly.
	 * </p>
	 * 
	 * <p>
	 * Transient members will be not be included, as they are likely derived.
	 * Static fields will not be included. Superclass fields will be appended.
	 * </p>
	 * 
	 * @param object
	 *            the Object to be output
	 * @return the String result
	 * @throws IllegalArgumentException
	 *             if the Object is <code>null</code>
	 */
	public static String toString(Object object) {
		return toString(object, null, false, false, null);
	}

	/**
	 * <p>
	 * Builds a <code>toString</code> value through reflection.
	 * </p>
	 * 
	 * <p>
	 * It uses <code>AccessibleObject.setAccessible</code> to gain access to
	 * private fields. This means that it will throw a security exception if run
	 * under a security manager, if the permissions are not set up correctly. It
	 * is also not as efficient as testing explicitly.
	 * </p>
	 * 
	 * <p>
	 * Transient members will be not be included, as they are likely derived.
	 * Static fields will not be included. Superclass fields will be appended.
	 * </p>
	 * 
	 * <p>
	 * If the style is <code>null</code>, the default <code>ToStringStyle</code>
	 * is used.
	 * </p>
	 * 
	 * @param object
	 *            the Object to be output
	 * @param style
	 *            the style of the <code>toString</code> to create, may be
	 *            <code>null</code>
	 * @return the String result
	 * @throws IllegalArgumentException
	 *             if the Object or <code>ToStringStyle</code> is
	 *             <code>null</code>
	 */
	public static String toString(Object object, ToStringStyle style) {
		return toString(object, style, false, false, null);
	}

	/**
	 * <p>
	 * Builds a <code>toString</code> value through reflection.
	 * </p>
	 * 
	 * <p>
	 * It uses <code>AccessibleObject.setAccessible</code> to gain access to
	 * private fields. This means that it will throw a security exception if run
	 * under a security manager, if the permissions are not set up correctly. It
	 * is also not as efficient as testing explicitly.
	 * </p>
	 * 
	 * <p>
	 * If the <code>outputTransients</code> is <code>true</code>, transient
	 * members will be output, otherwise they are ignored, as they are likely
	 * derived fields, and not part of the value of the Object.
	 * </p>
	 * 
	 * <p>
	 * Static fields will not be included. Superclass fields will be appended.
	 * </p>
	 * 
	 * <p>
	 * If the style is <code>null</code>, the default <code>ToStringStyle</code>
	 * is used.
	 * </p>
	 * 
	 * @param object
	 *            the Object to be output
	 * @param style
	 *            the style of the <code>toString</code> to create, may be
	 *            <code>null</code>
	 * @param outputTransients
	 *            whether to include transient fields
	 * @return the String result
	 * @throws IllegalArgumentException
	 *             if the Object is <code>null</code>
	 */
	public static String toString(Object object, ToStringStyle style, boolean outputTransients) {
		return toString(object, style, outputTransients, false, null);
	}

	/**
	 * <p>
	 * Builds a <code>toString</code> value through reflection.
	 * </p>
	 * 
	 * <p>
	 * It uses <code>AccessibleObject.setAccessible</code> to gain access to
	 * private fields. This means that it will throw a security exception if run
	 * under a security manager, if the permissions are not set up correctly. It
	 * is also not as efficient as testing explicitly.
	 * </p>
	 * 
	 * <p>
	 * If the <code>outputTransients</code> is <code>true</code>, transient
	 * fields will be output, otherwise they are ignored, as they are likely
	 * derived fields, and not part of the value of the Object.
	 * </p>
	 * 
	 * <p>
	 * If the <code>outputStatics</code> is <code>true</code>, static fields
	 * will be output, otherwise they are ignored.
	 * </p>
	 * 
	 * <p>
	 * Static fields will not be included. Superclass fields will be appended.
	 * </p>
	 * 
	 * <p>
	 * If the style is <code>null</code>, the default <code>ToStringStyle</code>
	 * is used.
	 * </p>
	 * 
	 * @param object
	 *            the Object to be output
	 * @param style
	 *            the style of the <code>toString</code> to create, may be
	 *            <code>null</code>
	 * @param outputTransients
	 *            whether to include transient fields
	 * @param outputStatics
	 *            whether to include transient fields
	 * @return the String result
	 * @throws IllegalArgumentException
	 *             if the Object is <code>null</code>
	 * @since 2.1
	 */
	public static String toString(Object object, ToStringStyle style, boolean outputTransients,
			boolean outputStatics) {
		return toString(object, style, outputTransients, outputStatics, null);
	}

	/**
	 * <p>
	 * Builds a <code>toString</code> value through reflection.
	 * </p>
	 * 
	 * <p>
	 * It uses <code>AccessibleObject.setAccessible</code> to gain access to
	 * private fields. This means that it will throw a security exception if run
	 * under a security manager, if the permissions are not set up correctly. It
	 * is also not as efficient as testing explicitly.
	 * </p>
	 * 
	 * <p>
	 * If the <code>outputTransients</code> is <code>true</code>, transient
	 * fields will be output, otherwise they are ignored, as they are likely
	 * derived fields, and not part of the value of the Object.
	 * </p>
	 * 
	 * <p>
	 * If the <code>outputStatics</code> is <code>true</code>, static fields
	 * will be output, otherwise they are ignored.
	 * </p>
	 * 
	 * <p>
	 * Superclass fields will be appended up to and including the specified
	 * superclass. A null superclass is treated as <code>java.lang.Object</code>
	 * .
	 * </p>
	 * 
	 * <p>
	 * If the style is <code>null</code>, the default <code>ToStringStyle</code>
	 * is used.
	 * </p>
	 * 
	 * @param object
	 *            the Object to be output
	 * @param style
	 *            the style of the <code>toString</code> to create, may be
	 *            <code>null</code>
	 * @param outputTransients
	 *            whether to include transient fields
	 * @param outputStatics
	 *            whether to include static fields
	 * @param reflectUpToClass
	 *            the superclass to reflect up to (inclusive), may be
	 *            <code>null</code>
	 * @return the String result
	 * @throws IllegalArgumentException
	 *             if the Object is <code>null</code>
	 * @since 2.1
	 */
	public static <T> String toString(T object, ToStringStyle style, boolean outputTransients,
			boolean outputStatics, Class<? super T> reflectUpToClass) {
		return new ReflectionToStringBuilder(object, style, null, reflectUpToClass, outputTransients,
				outputStatics).toString();
	}

	/**
	 * Builds a String for a toString method excluding the given field name.
	 * 
	 * @param object
	 *            The object to "toString".
	 * @param excludeFieldName
	 *            The field name to exclude
	 * @return The toString value.
	 */
	public static String toStringExclude(Object object, final String excludeFieldName) {
		return toStringExclude(object, new String[] { excludeFieldName });
	}

	/**
	 * Builds a String for a toString method excluding the given field names.
	 * 
	 * @param object
	 *            The object to "toString".
	 * @param excludeFieldNames
	 *            The field names to exclude. Null excludes nothing.
	 * @return The toString value.
	 */
	public static String toStringExclude(Object object, Collection<String> excludeFieldNames) {
		return toStringExclude(object, toNoNullStringArray(excludeFieldNames));
	}

	/**
	 * Converts the given Collection into an array of Strings. The returned
	 * array does not contain <code>null</code> entries. Note that
	 * {@link Arrays#sort(Object[])} will throw an {@link NullPointerException}
	 * if an array element
	 * is <code>null</code>.
	 * 
	 * @param collection
	 *            The collection to convert
	 * @return A new array of Strings.
	 */
	static String[] toNoNullStringArray(Collection<String> collection) {
		if(collection == null){
			return ArrayUtils.EMPTY_STRING_ARRAY;
		}
		return toNoNullStringArray(collection.toArray());
	}

	/**
	 * Returns a new array of Strings without null elements. Internal method
	 * used to normalize exclude lists
	 * (arrays and collections). Note that {@link Arrays#sort(Object[])} will
	 * throw an {@link NullPointerException} if an array element is
	 * <code>null</code>.
	 * 
	 * @param array
	 *            The array to check
	 * @return The given array or a new array without null.
	 */
	static String[] toNoNullStringArray(Object[] array) {
		List<String> list = new ArrayList<String>(array.length);
		for(Object e : array){
			if(e != null){
				list.add(e.toString());
			}
		}
		return list.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
	}

	/**
	 * Builds a String for a toString method excluding the given field names.
	 * 
	 * @param object
	 *            The object to "toString".
	 * @param excludeFieldNames
	 *            The field names to exclude
	 * @return The toString value.
	 */
	public static String toStringExclude(Object object, String[] excludeFieldNames) {
		return new ReflectionToStringBuilder(object).setExcludeFieldNames(excludeFieldNames).toString();
	}

	/**
	 * Whether or not to append static fields.
	 */
	private boolean appendStatics = false;

	/**
	 * Whether or not to append transient fields.
	 */
	private boolean appendTransients = false;

	/**
	 * Which field names to exclude from output. Intended for fields like
	 * <code>"password"</code>.
	 */
	protected String[] excludeFieldNames;

	/**
	 * The last super class to stop appending fields for.
	 */
	private Class<?> upToClass = null;

	/**
	 * <p>
	 * Constructor.
	 * </p>
	 * 
	 * <p>
	 * This constructor outputs using the default style set with
	 * <code>setDefaultStyle</code>.
	 * </p>
	 * 
	 * @param object
	 *            the Object to build a <code>toString</code> for, must not be
	 *            <code>null</code>
	 * @throws IllegalArgumentException
	 *             if the Object passed in is <code>null</code>
	 */
	public ReflectionToStringBuilder(Object object) {
		super(object);
	}

	/**
	 * <p>
	 * Constructor.
	 * </p>
	 * 
	 * <p>
	 * If the style is <code>null</code>, the default style is used.
	 * </p>
	 * 
	 * @param object
	 *            the Object to build a <code>toString</code> for, must not be
	 *            <code>null</code>
	 * @param style
	 *            the style of the <code>toString</code> to create, may be
	 *            <code>null</code>
	 * @throws IllegalArgumentException
	 *             if the Object passed in is <code>null</code>
	 */
	public ReflectionToStringBuilder(Object object, ToStringStyle style) {
		super(object, style);
	}

	/**
	 * <p>
	 * Constructor.
	 * </p>
	 * 
	 * <p>
	 * If the style is <code>null</code>, the default style is used.
	 * </p>
	 * 
	 * <p>
	 * If the buffer is <code>null</code>, a new one is created.
	 * </p>
	 * 
	 * @param object
	 *            the Object to build a <code>toString</code> for
	 * @param style
	 *            the style of the <code>toString</code> to create, may be
	 *            <code>null</code>
	 * @param buffer
	 *            the <code>StringBuffer</code> to populate, may be
	 *            <code>null</code>
	 * @throws IllegalArgumentException
	 *             if the Object passed in is <code>null</code>
	 */
	public ReflectionToStringBuilder(Object object, ToStringStyle style, StringBuffer buffer) {
		super(object, style, buffer);
	}

	/**
	 * Constructor.
	 * 
	 * @param object
	 *            the Object to build a <code>toString</code> for
	 * @param style
	 *            the style of the <code>toString</code> to create, may be
	 *            <code>null</code>
	 * @param buffer
	 *            the <code>StringBuffer</code> to populate, may be
	 *            <code>null</code>
	 * @param reflectUpToClass
	 *            the superclass to reflect up to (inclusive), may be
	 *            <code>null</code>
	 * @param outputTransients
	 *            whether to include transient fields
	 * @param outputStatics
	 *            whether to include static fields
	 * @since 2.1
	 */
	public <T> ReflectionToStringBuilder(T object, ToStringStyle style, StringBuffer buffer,
			Class<? super T> reflectUpToClass, boolean outputTransients, boolean outputStatics) {
		super(object, style, buffer);
		this.setUpToClass(reflectUpToClass);
		this.setAppendTransients(outputTransients);
		this.setAppendStatics(outputStatics);
	}

	/**
	 * Returns whether or not to append the given <code>Field</code>.
	 * <ul>
	 * <li>Transient fields are appended only if {@link #isAppendTransients()}
	 * returns <code>true</code>.
	 * <li>Static fields are appended only if {@link #isAppendStatics()} returns
	 * <code>true</code>.
	 * <li>Inner class fields are not appened.</li>
	 * </ul>
	 * 
	 * @param field
	 *            The Field to test.
	 * @return Whether or not to append the given <code>Field</code>.
	 */
	protected boolean accept(Field field) {
		if(field.getName().indexOf(ClassUtils.INNER_CLASS_SEPARATOR_CHAR) != -1){
			// Reject field from inner class.
			return false;
		}
		if(Modifier.isTransient(field.getModifiers()) && !this.isAppendTransients()){
			// Reject transient fields.
			return false;
		}
		if(Modifier.isStatic(field.getModifiers()) && !this.isAppendStatics()){
			// Reject static fields.
			return false;
		}
		if(this.excludeFieldNames != null
				&& Arrays.binarySearch(this.excludeFieldNames, field.getName()) >= 0){
			// Reject fields from the getExcludeFieldNames list.
			return false;
		}
		return true;
	}

	/**
	 * <p>
	 * Appends the fields and values defined by the given object of the given
	 * Class.
	 * </p>
	 * 
	 * <p>
	 * If a cycle is detected as an object is &quot;toString()'ed&quot;, such an
	 * object is rendered as if <code>Object.toString()</code> had been called
	 * and not implemented by the object.
	 * </p>
	 * 
	 * @param clazz
	 *            The class of object parameter
	 */
	protected void appendFieldsIn(Class<?> clazz) {
		if(clazz.isArray()){
			this.reflectionAppendArray(this.getObject());
			return;
		}
		Field[] fields = clazz.getDeclaredFields();
		AccessibleObject.setAccessible(fields, true);
		for(Field field : fields){
			String fieldName = field.getName();
			if(this.accept(field)){
				try{
					// Warning: Field.get(Object) creates wrappers objects
					// for primitive types.
					Object fieldValue = this.getValue(field);
					this.append(fieldName, fieldValue);
				} catch(IllegalAccessException ex){
					// this can't happen. Would get a Security exception
					// instead
					// throw a runtime exception in case the impossible
					// happens.
					throw new InternalError("Unexpected IllegalAccessException: " + ex.getMessage());
				}
			}
		}
	}

	/**
	 * @return Returns the excludeFieldNames.
	 */
	public String[] getExcludeFieldNames() {
		return this.excludeFieldNames.clone();
	}

	/**
	 * <p>
	 * Gets the last super class to stop appending fields for.
	 * </p>
	 * 
	 * @return The last super class to stop appending fields for.
	 */
	public Class<?> getUpToClass() {
		return this.upToClass;
	}

	/**
	 * <p>
	 * Calls <code>java.lang.reflect.Field.get(Object)</code>.
	 * </p>
	 * 
	 * @param field
	 *            The Field to query.
	 * @return The Object from the given Field.
	 * 
	 * @throws IllegalArgumentException
	 *             see {@link java.lang.reflect.Field#get(Object)}
	 * @throws IllegalAccessException
	 *             see {@link java.lang.reflect.Field#get(Object)}
	 * 
	 * @see java.lang.reflect.Field#get(Object)
	 */
	protected Object getValue(Field field) throws IllegalArgumentException, IllegalAccessException {
		return field.get(this.getObject());
	}

	/**
	 * <p>
	 * Gets whether or not to append static fields.
	 * </p>
	 * 
	 * @return Whether or not to append static fields.
	 * @since 2.1
	 */
	public boolean isAppendStatics() {
		return this.appendStatics;
	}

	/**
	 * <p>
	 * Gets whether or not to append transient fields.
	 * </p>
	 * 
	 * @return Whether or not to append transient fields.
	 */
	public boolean isAppendTransients() {
		return this.appendTransients;
	}

	/**
	 * <p>
	 * Append to the <code>toString</code> an <code>Object</code> array.
	 * </p>
	 * 
	 * @param array
	 *            the array to add to the <code>toString</code>
	 * @return this
	 */
	public ReflectionToStringBuilder reflectionAppendArray(Object array) {
		this.getStyle().reflectionAppendArrayDetail(this.getStringBuffer(), null, array);
		return this;
	}

	/**
	 * <p>
	 * Sets whether or not to append static fields.
	 * </p>
	 * 
	 * @param appendStatics
	 *            Whether or not to append static fields.
	 * @since 2.1
	 */
	public void setAppendStatics(boolean appendStatics) {
		this.appendStatics = appendStatics;
	}

	/**
	 * <p>
	 * Sets whether or not to append transient fields.
	 * </p>
	 * 
	 * @param appendTransients
	 *            Whether or not to append transient fields.
	 */
	public void setAppendTransients(boolean appendTransients) {
		this.appendTransients = appendTransients;
	}

	/**
	 * Sets the field names to exclude.
	 * 
	 * @param excludeFieldNamesParam
	 *            The excludeFieldNames to excluding from toString or
	 *            <code>null</code>.
	 * @return <code>this</code>
	 */
	public ReflectionToStringBuilder setExcludeFieldNames(String[] excludeFieldNamesParam) {
		if(excludeFieldNamesParam == null){
			this.excludeFieldNames = null;
		} else{
			this.excludeFieldNames = toNoNullStringArray(excludeFieldNamesParam);
			Arrays.sort(this.excludeFieldNames);
		}
		return this;
	}

	/**
	 * <p>
	 * Sets the last super class to stop appending fields for.
	 * </p>
	 * 
	 * @param clazz
	 *            The last super class to stop appending fields for.
	 */
	public void setUpToClass(Class<?> clazz) {
		if(clazz != null){
			Object object = getObject();
			if(object != null && clazz.isInstance(object) == false){
				throw new IllegalArgumentException("Specified class is not a superclass of the object");
			}
		}
		this.upToClass = clazz;
	}

	/**
	 * <p>
	 * Gets the String built by this builder.
	 * </p>
	 * 
	 * @return the built string
	 */
	@Override
	public String toString() {
		if(this.getObject() == null){
			return this.getStyle().getNullText();
		}
		Class<?> clazz = this.getObject().getClass();
		this.appendFieldsIn(clazz);
		while(clazz.getSuperclass() != null && clazz != this.getUpToClass()){
			clazz = clazz.getSuperclass();
			this.appendFieldsIn(clazz);
		}
		return super.toString();
	}

}
